<?php
/**
 * Copyright (c) Enalean 2017-2018. All rights reserved
 * Copyright (c) Xerox Corporation, Codendi Team, 2001-2009. All rights reserved
 *
 * This file is a part of Tuleap.
 *
 * Tuleap is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Tuleap is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Tuleap. If not, see <http://www.gnu.org/licenses/>.
 */

require_once('common/valid/Rule.class.php');
require_once('common/include/Codendi_HTTPPurifier.class.php');

class Tracker_FormElement_Field_File extends Tracker_FormElement_Field {
    const SOAP_FAKE_FILE = 'soapfakefile';
    const SOAP_FAULT_INVALID_REQUEST_FORMAT = '3029';

    public function getCriteriaFrom($criteria) {
        //Only filter query if field  is used
        if($this->isUsed()) {
            //Only filter query if criteria is valuated
            if ($criteria_value = $this->getCriteriaValue($criteria)) {
                $a = 'A_'. $this->id;
                $b = 'B_'. $this->id;
                $c = 'C_'. $this->id;

                $da             = CodendiDataAccess::instance();
                $criteria_value = $da->quoteLikeValueSurround($criteria_value);

                return " INNER JOIN tracker_changeset_value AS $a ON ($a.changeset_id = c.id AND $a.field_id = $this->id )
                         INNER JOIN tracker_changeset_value_file AS $b ON ($b.changeset_value_id = $a.id)
                         INNER JOIN tracker_fileinfo AS $c ON (
                            $c.id = $b.fileinfo_id
                            AND (
                                $c.description LIKE ". $criteria_value ."
                                OR
                                $c.filename LIKE ". $criteria_value ."
                            )
                         ) ";
            }
        }
        return '';
    }

    public function getCriteriaWhere($criteria) {
        return '';
    }

    public function getQuerySelect() {
        return '';
    }

    public function getQueryFrom() {
        return '';
    }

    protected function getCriteriaDao() {
        return new Tracker_Report_Criteria_File_ValueDao();
    }

    public function fetchChangesetValue($artifact_id, $changeset_id, $value, $report=null, $from_aid = null) {
        $html = '';
        $submitter_needed = true;
        $html .= $this->fetchAllAttachment($artifact_id, $this->getChangesetValues($changeset_id), $submitter_needed, array());
        return $html;
    }

    /**
     * Display the field as a Changeset value.
     * Used in CSV data export.
     *
     * @param int $artifact_id the corresponding artifact id
     * @param int $changeset_id the corresponding changeset
     * @param mixed $value the value of the field
     *
     * @return string
     */
    public function fetchCSVChangesetValue($artifact_id, $changeset_id, $value, $report) {
        return $this->fetchAllAttachmentForCSV($artifact_id, $this->getChangesetValues($changeset_id));
    }

    public function fetchCriteriaValue($criteria) {
        $html = '<input type="text" name="criteria['. $this->id .']" id="tracker_report_criteria_'. $this->id .'" value="';
        if ($criteria_value = $this->getCriteriaValue($criteria)) {
            $hp = Codendi_HTMLPurifier::instance();
            $html .= $hp->purify($criteria_value, CODENDI_PURIFIER_CONVERT_HTML);
        }
        $html .= '" />';
        return $html;
    }

    /**
     * Fetch the value
     * @param mixed $value the value of the field
     * @return string
     */
    public function fetchRawValue($value) {
        return $value;
    }

    /**
     * Fetch the value in a specific changeset
     * @param Tracker_Artifact_Changeset $changeset
     * @return string
     */
    public function fetchRawValueFromChangeset($changeset) {
        $value = '';
        if ($v = $changeset->getValue($this)) {
            if (isset($v['value_id'])) {
                $v = array($v);
            }
            foreach ($v as $val) {
                if ($val['value_id'] != 100) {
                    if ($row = $this->getValueDao()->searchById($val['value_id'], $this->id)->getRow()) {
                        if ($value) {
                            $value .= ', ';
                        }
                        $value .= $row['filename'];
                    }
                }
            }
        }
        return $value;
    }

    protected function getValueDao() {
        return new Tracker_FormElement_Field_Value_FileDao();
    }


    /**
     * Fetch the html code to display the field value in artifact
     *
     * @param Tracker_Artifact                $artifact         The artifact
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
     * @param array                           $submitted_values The value already submitted by the user
     *
     * @return string
     */
    protected function fetchArtifactValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
        $html             = '';
        $submitter_needed = true;
        $read_only        = false;
        $html .= $this->fetchAllAttachment($artifact->id, $value, $submitter_needed, $submitted_values, $read_only);
        $html .= $this->fetchSubmitValue();
        return $html;
    }

    public function fetchArtifactForOverlay(Tracker_Artifact $artifact) {
        return $this->fetchArtifactReadOnly($artifact);
    }

    public function fetchSubmitForOverlay($submitted_values) {
        return '';
    }

    public function fetchArtifactCopyMode(Tracker_Artifact $artifact) {
        $last_changeset = $artifact->getLastChangeset();
        if ($last_changeset) {
            $value = $last_changeset->getValue($this);
            return $this->fetchAllAttachmentTitleAndDescription($value);
        }
        return '';
    }

    /**
     * Fetch the html code to display the field value in Mail
     *
     * @param Tracker_Artifact                $artifact         The artifact
     * @param PFUser                          $user             The user who will receive the email
     * @param boolean                         $ignore_perms
     * @param Tracker_Artifact_ChangesetValue $value            The actual value of the field
     * @param array                           $submitted_values The value already submitted by the user
     *
     * @return string
     */
    public function fetchMailArtifactValue(
        Tracker_Artifact $artifact,
        PFUser $user,
        $ignore_perms,
        Tracker_Artifact_ChangesetValue $value = null,
        $format = 'text'
    ) {
        if ( empty($value) || ! $value->getFiles()) {
            return '-';
        }

        return $this->fetchMailAllAttachment($artifact->id, $value, $format);
    }

    /**
     * Fetch the html code to display the field value in artifact in read only mode
     *
     * @param Tracker_Artifact                $artifact The artifact
     * @param Tracker_Artifact_ChangesetValue $value    The actual value of the field
     *
     * @return string
     */
    public function fetchArtifactValueReadOnly(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
        $html = '';
        $submitter_needed = true;
        $html .= $this->fetchAllAttachment($artifact->id, $value, $submitter_needed, array());
        return $html;
    }

    public function fetchArtifactValueWithEditionFormIfEditable(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null, $submitted_values = array()) {
        return $this->fetchArtifactValueReadOnly($artifact, $value) . $this->getHiddenArtifactValueForEdition($artifact, $value, $submitted_values);
    }
    /**
     * Fetch the html code to display the field value in new artifact submission form
     *
     * @return string html
     */
    protected function fetchSubmitValue() {
        $html = '';
        $html .= '<div class="add-attachement">';
        $html .= '<p>'. $GLOBALS['Language']->getText('plugin_tracker_formelement_admin','add_new_file') .'</p>';
        $html .= '<div class="tracker_artifact_add_attachment">';
        $html .= '<p>';
        $html .= '<input type="file" id="tracker_field_'. $this->id .'" name="artifact['. $this->id .'][][file]" />';
        $html .= '<label>'. $GLOBALS['Language']->getText('plugin_tracker_formelement_admin','add_new_file_description');
        $html .= '</label>';
        $html .= ' <input type="text" id="tracker_field_'. $this->id .'" name="artifact['. $this->id .'][][description]" />';
        $html .= '</p>';
        $html .= '</div>';
        $html .= '</div>';
        return $html;
    }

    /**
     * Fetch the html code to display the field value in new artifact submission form
     *
     * @return string html
     */
    protected function fetchSubmitValueMasschange() {
        return '';  // deactivate mass change for file fields (see issue described in rev #15855)
    }

    /**
     * Fetch the changes that has been made to this field in a followup
     *
     * @param Tracker_Artifact $artifact The artifact
     * @param array            $from     the value(s) *before*
     * @param array            $to       the value(s) *after*
     *
     * @return string html
     */
    public function fetchFollowUp($artifact, $from, $to) {
        $html = '';
        //Retrieve all the values for the changeset
        $to_values = $this->getChangesetValues($to['changeset_id']);
        foreach ($to_values as $key => $v) {
            if (!$v['has_changed']) {
                unset($to_values[$key]);
            }
        }
        if (count($to_values)) {
            $submitter_needed = false;
            $html .= 'Added: '. $this->fetchAllAttachment($artifact->id, $to_values, $submitter_needed, array());
        }
        return $html;
    }

    public function fetchAllAttachment($artifact_id, $values, $submitter_needed, $submitted_values, $read_only = true, $lytebox_id = null) {
        $html = '';
        if ($lytebox_id === null) {
            $lytebox_id = $this->getId();
        }
        if (count($values)) {
            $hp = Codendi_HTMLPurifier::instance();
            $uh = UserHelper::instance();
            $added = array();
            foreach ($values as $fileinfo) {
                $query_link = $this->getFileHTMLUrl($fileinfo);
                $sanitized_description = $hp->purify($fileinfo->getDescription(), CODENDI_PURIFIER_CONVERT_HTML);
               
                $link_show = '<a href="'. $query_link .'"'.
                                 $this->getVisioningAttributeForLink($fileinfo, $read_only, $lytebox_id) .'
                                 title="'. $sanitized_description .'">';

                $add = '<div class="tracker_artifact_attachment">';
                if (!$read_only) {
                    $add .= $this->fetchDeleteCheckbox($fileinfo, $submitted_values);
                }


                $add .= '<div class="tracker_artifact_preview_attachment_hover">';
                if ($submitter_needed) {
                    $add .= '<div class="tracker_artifact_attachment_submitter">'. 'By '. $uh->getLinkOnUserFromUserId($fileinfo->getSubmittedBy()) .'</div>';
                }
                $add .= '<div class="tracker_artifact_attachment_size">('. $fileinfo->getHumanReadableFilesize() .')</div>';
                $add .= '<div>';
                $add .= $link_show . '<i class="icon-eye-open"></i></a>';
                $add .= '<a href="'. $query_link .'" download><i class="icon-download-alt"></i></a>';
                $add .= '</div>';
                $add .= '</div>';

                if ($fileinfo->isImage()) {
                    $query_add = $this->getFileHTMLPreviewUrl($fileinfo);

                    $add .= '<div class="tracker_artifact_preview_attachment image">';
                    $add .= '<div style="background-image: url(\''. $query_add .'\')"></div>';
                    $add .= '</div>';
                } else {
                    $add .= '<div class="tracker_artifact_preview_attachment"></div>';
                }


                $link_goto = '<a href="'. $query_link .'"'.
                                 'title="'. $sanitized_description .'">';
                $add .= '<div class="tracker_artifact_attachment_name">' . $link_goto . $hp->purify($fileinfo->getFilename(), CODENDI_PURIFIER_CONVERT_HTML) .'</a></div>';

                if ($sanitized_description) {
                    $add .= '<div class="tracker_artifact_attachment_description">' . $sanitized_description . '</div>';
                }

                $add .= '</div>';
                $added[] = $add;
            }
            $html .= implode('', $added);
        }

        if ($read_only && ! count($values)) {
            $html .= $this->getNoValueLabel();
        }

        return $html;
    }

    public function getFileHTMLUrl(Tracker_FileInfo $file_info) {
        $artifact = $this->getFileInfoFactory()->getArtifactByFileInfoId($file_info->getId());
        if (!$artifact) {
            return;
        }

        return TRACKER_BASE_URL . '/?' . http_build_query(
            array(
                'aid'   => $artifact->getId(),
                'field' => $this->id,
                'func'  => 'show-attachment',
                'attachment' => $file_info->getId()
            )
        );
    }

    public function getFileHTMLPreviewUrl(Tracker_FileInfo $file_info) {
        if (! $file_info->isImage()) {
            return;
        }

        $artifact = $this->getFileInfoFactory()->getArtifactByFileInfoId($file_info->getId());

        if (!$artifact) {
            return;
        }

        return TRACKER_BASE_URL . '/?' . http_build_query(
            array(
                'aid'   => $artifact->getId(),
                'field' => $this->id,
                'func'  => 'preview-attachment',
                'attachment' => $file_info->getId()
            )
        );
    }

    private function getVisioningAttributeForLink($fileinfo, $read_only, $lytebox_id) {
        if (! $fileinfo->isImage()) {
            return '';
        }

        if ($read_only) {
            return 'rel="lytebox['. $lytebox_id .']"';
        }

        return 'data-rel="lytebox['. $lytebox_id .']"';
    }

    private function fetchDeleteCheckbox(Tracker_FileInfo $fileinfo, $submitted_values) {
        $html = '';
        $html .= '<label class="pc_checkbox tracker_artifact_attachment_delete">';
        $checked = '';
        if (isset($submitted_values[0][$this->id]) && ! empty($submitted_values[0][$this->id]['delete']) && in_array($fileinfo->getId(), $submitted_values[0][$this->id]['delete'])) {
            $checked = 'checked="checked"';
        }
        $html .= '<input type="checkbox" name="artifact['. $this->id .'][delete][]" value="'. $fileinfo->getId() .'" title="delete" '. $checked .' />&nbsp;';
        $html .= '</label>';
        return $html;
    }

    protected function fetchAllAttachmentForCSV($artifact_id, $values) {
        $txt = '';
        if (count($values)) {
            $filenames = array();
            foreach ($values as $fileinfo) {
                $filenames[] = $fileinfo->getFilename();
            }
            $txt .= implode(',', $filenames);
        }
        return $txt;
    }

    protected function fetchAllAttachmentTitleAndDescription($values) {
        $html = '';
        if($values) {
            $purifier = Codendi_HTMLPurifier::instance();
            $html    .= '<div class="tracker-artifact-attachement-title-list tracker_artifact_field"
                              data-field-id="'. $this->id .'"
                              data-is-required="false">';
            $html    .= '<div class="disabled_field">' . $GLOBALS['Language']->getText('plugin_tracker_artifact', 'formelement_attachment_copy') . '</div>';
            $html    .= '<ul>';
            foreach($values as $value) {
                $description = $value->getDescription();

                $html .= '<li>';
                $html .= '<span class="file-title">';
                $html .= $purifier->purify($value->getFileName());
                $html .= '</span>';

                if($description) {
                    $html .= '<span class="file-description">';
                    $html .= ' - ' . $purifier->purify($description);
                    $html .= '</span>';
                }
                $html .= '</li>';
            }
            $html .= '</ul>';
            $html .= '</div>';
        }
        return $html;
    }

    /**
     * Fetch all attachements for Mail output
     *
     * @param Integer $artifact_id The artifact Id
     * @param Array            $values     The actual value of the field
     * @param String            $format       The mail format
     *
     * @return String
     */
    protected function fetchMailAllAttachment($artifact_id, $values, $format) {
        $output = '';
        if (!count($values) ) {
            return '';
        }

        $uh = UserHelper::instance();

        $proto = ForgeConfig::get('sys_https_host') ? 'https' : 'http';
        $url = $proto .'://'. $GLOBALS['sys_default_domain'];

        if ($format == 'text') {
            foreach ($values as $fileinfo) {
                $query_link = $this->getFileHTMLUrl($fileinfo);

                $link = '<'.$url.$query_link.'>';
                $output .= $fileinfo->getDescription();
                $output .= ' | ';
                $output .= $fileinfo->getFilename();
                $output .= ' | ';
                $output .= $fileinfo->getHumanReadableFilesize();
                $output .= ' | ';
                $output .= $uh->getDisplayNameFromUserId( $fileinfo->getSubmittedBy() );
                $output .= PHP_EOL;
                $output .= $link;
                $output .= PHP_EOL;
            }
        } else {
            $hp = Codendi_HTMLPurifier::instance();
            $added = array();
            foreach ($values as $fileinfo) {
                $query_link = $this->getFileHTMLUrl($fileinfo);
                $sanitized_description = $hp->purify($fileinfo->getDescription(), CODENDI_PURIFIER_CONVERT_HTML);
                $link_show = '<a href="'.$url.$query_link .'"
                                 title="'. $sanitized_description .'">';

                $info = $link_show . $hp->purify($fileinfo->getFilename(), CODENDI_PURIFIER_CONVERT_HTML) .'</a>';
                $info .= ' ('. $fileinfo->getHumanReadableFilesize() .')';

                $add = '<div class="tracker_artifact_attachment">';
                $add .= '<table><tr><td>';
                $add .= $info;
                $add .= '</td></tr></table>';
                $add .= '</div>';
                $added[] = $add;
            }
            $output .= implode('', $added);
        }
        return $output;
    }

    protected $file_values_by_changeset;

    /**
     * @return array
     */
    protected function getChangesetValues($changeset_id) {
        $da = CodendiDataAccess::instance();
        if (!$this->file_values_by_changeset) {
            $this->file_values_by_changeset = array();
            $field_id     = $da->escapeInt($this->id);
            $sql = "SELECT c.changeset_id, c.has_changed, f.id
                    FROM tracker_fileinfo as f
                         INNER JOIN tracker_changeset_value_file AS vf on (f.id = vf.fileinfo_id)
                         INNER JOIN tracker_changeset_value AS c
                         ON ( vf.changeset_value_id = c.id
                          AND c.field_id = $field_id
                         )
                    ORDER BY f.id";
            $dao               = new DataAccessObject();
            $file_info_factory = $this->getTrackerFileInfoFactory();
            foreach ($dao->retrieve($sql) as $row) {
                $this->file_values_by_changeset[$row['changeset_id']][] = $file_info_factory->getById($row['id']);
            }
        }
        return isset($this->file_values_by_changeset[$changeset_id]) ? $this->file_values_by_changeset[$changeset_id] : array();
    }

    public function previewAttachment($attachment_id) {
        if ($fileinfo = $this->getTrackerFileInfoFactory()->getById($attachment_id)) {
            if ($fileinfo->isImage() && file_exists($fileinfo->getThumbnailPath())) {
                header('Content-type: '. $fileinfo->getFiletype());
                readfile($fileinfo->getThumbnailPath());
            }
        }
        exit();
    }

    public function showAttachment($attachment_id) {
        if ($fileinfo = $this->getTrackerFileInfoFactory()->getById($attachment_id)) {
            if ($fileinfo->fileExists()) {
                $http = Codendi_HTTPPurifier::instance();
                header('X-Content-Type-Options: nosniff');
                header('Content-Type: '.$http->purify($fileinfo->getFiletype()));
                header('Content-Length: '.$http->purify($fileinfo->getFilesize()));
                header('Content-Disposition: attachment; filename="'.$http->purify($fileinfo->getFilename()).'"');
                header('Content-Description: '. $http->purify($fileinfo->getDescription()));
                if (ob_get_level()) {
                    ob_end_clean();
                }
                flush();
                $file = fopen($fileinfo->getPath(), "r");
                while (! feof($file)) {
                    print fread($file, 30*1024);
                    flush();
                }
                fclose($file);
            }
        }
        exit();
    }

    public function getRootPath() {
        return $this->getGlobalTrackerRootPath() . $this->getId();
    }

    /**
     * Display the html field in the admin ui
     *
     * @return string html
     */
    protected function fetchAdminFormElement() {
        $html = '';
        $html .= '<div>';
        $html .= '<p>'.$GLOBALS['Language']->getText('plugin_tracker_formelement_admin','add_new_file').'</p>';
        $html .= '<table class="tracker_artifact_add_attachment">';
        $html .= '<tr><td><label>'.$GLOBALS['Language']->getText('plugin_tracker_formelement_admin','add_new_file_description').'</label></td><td><label>'.$GLOBALS['Language']->getText('plugin_tracker_formelement_admin','add_new_file_file').'</label></td></tr>';
        $html .= '<tr><td><input type="text" id="tracker_field_'. $this->id .'" /></td>';
        $html .= '<td><input type="file" id="tracker_field_'. $this->id .'" /></td></tr>';
        $html .= '</table>';
        $html .= '</div>';
        return $html;
    }

    /**
     * @return the label of the field (mainly used in admin part)
     */
    public static function getFactoryLabel() {
        return $GLOBALS['Language']->getText('plugin_tracker_formelement_admin','file');
    }

    /**
     * @return the description of the field (mainly used in admin part)
     */
    public static function getFactoryDescription() {
        return $GLOBALS['Language']->getText('plugin_tracker_formelement_admin','file_description');
    }

    /**
     * @return the path to the icon
     */
    public static function getFactoryIconUseIt() {
        return $GLOBALS['HTML']->getImagePath('ic/attach.png');
    }

    /**
     * @return the path to the icon
     */
    public static function getFactoryIconCreate() {
        return $GLOBALS['HTML']->getImagePath('ic/attach--plus.png');
    }

    /**
     * Fetch the html code to display the field value in tooltip
     *
     * @param Tracker_Artifact            $artifact The artifact
     * @param Tracker_ChangesetValue_File $value    The changeset value of this field
     *
     * @return string The html code to display the field value in tooltip
     */
    protected function fetchTooltipValue(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $value = null) {
        $html = '';
        if ($value) {
            $files_info = $value->getFiles();
            if (count($files_info)) {
                $hp = Codendi_HTMLPurifier::instance();

                $added = array();
                foreach ($files_info as $file_info) {
                    $add = '';

                    if ($file_info->isImage()) {
                        $query = $this->getFileHTMLPreviewUrl($file_info);
                        $add .= '<img src="'.$query .'"
                                      alt="'.  $hp->purify($file_info->getDescription(), CODENDI_PURIFIER_CONVERT_HTML)  .'"
                                      style="vertical-align:middle;" />';
                    } else if ($file_info->getDescription()) {
                        $add .= $hp->purify($file_info->getDescription(), CODENDI_PURIFIER_CONVERT_HTML);
                    } else {
                        $add .= $hp->purify($file_info->getFilename(), CODENDI_PURIFIER_CONVERT_HTML);
                    }
                    $added[] = $add;
                }
                $html .= implode('<br />', $added);
            }
        }
        return $html;
    }

    /**
     * Validate a value
     *
     * @param Tracker_Artifact $artifact The artifact
     * @param mixed            $value    data coming from the request.
     *
     * @return bool true if the value is considered ok
     */
    protected function validate(Tracker_Artifact $artifact, $value) {
        return true;
    }

    /**
     * Say if the value is valid. If not valid set the internal has_error to true.
     *
     * @param Tracker_Artifact $artifact The artifact
     * @param mixed            $value    data coming from the request. May be string or array.
     *
     * @return bool true if the value is considered ok
     */
    public function isValid(Tracker_Artifact $artifact, $value) {
        $this->has_errors = false;

        if (is_array($value)) {
            $this->checkAllFilesHaveBeenSuccessfullyUploaded($value);
        }

        return !$this->has_errors;
    }

    private function checkAllFilesHaveBeenSuccessfullyUploaded($value) {
        $rule = new Rule_File();
        foreach($value as $i => $attachment) {
            if ($this->isAttachmentNeedsToBeValidated($i, $attachment)) {
                if (! $rule->isValid($attachment)) {
                    $this->has_errors = true;
                    $attachment_error = $GLOBALS['Language']->getText(
                        'plugin_tracker_formelement_file',
                        'attachment_in_error',
                        array($i)
                    );
                    $GLOBALS['Response']->addFeedback('error', $attachment_error . ' ' . $rule->getErrorMessage());
                }
            }
        }
    }

    /**
     * @return bool
     */
    private function isAttachmentNeedsToBeValidated($attachment_index, array $attachment) {
        if ($attachment_index === 'delete') {
            return false;
        }

        $is_file_uploaded              = ! empty($attachment['error']) && $attachment['error'] != UPLOAD_ERR_NO_FILE;
        $is_file_description_provided  = trim($attachment['description']);

        return $is_file_uploaded || $is_file_description_provided;
    }

    /**
     * Validate a required field
     *
     * @param Tracker_Artifact                $artifact             The artifact to check
     * @param mixed                           $value      The submitted value
     *
     * @return boolean true on success or false on failure
     */
    public function isValidRegardingRequiredProperty(Tracker_Artifact $artifact, $value) {
        $this->has_errors = false;

        if (is_array($value) &&
            $this->isRequired() &&
            ! $this->checkThatAtLeastOneFileIsUploaded($value) &&
            $this->isPreviousChangesetEmpty($artifact, $value)) {
            $this->addRequiredError();
        }

        return ! $this->has_errors;
    }

    /**
     * Check that at least one file is sent
     *
     * @param array $files the files
     *
     * @return bool true if success
     */
    public function checkThatAtLeastOneFileIsUploaded($files) {
        $r = new Rule_File();
        $a_file_is_sent = false;
        reset($files);
        while (!$a_file_is_sent && (list($action, $attachment) = each($files))) {
            if ("$action" != 'delete') {
                $a_file_is_sent = $r->isValid($attachment);
            }
        }
        return $a_file_is_sent;
    }

    /**
     * Extract data from request
     * Some fields like files doesn't have their value submitted in POST or GET
     * Let them populate $fields_data[field_id] if needed
     *
     * @param array &$fields_data The user submitted value
     *
     * @return void
     */
    public function augmentDataFromRequest(&$fields_data) {
        if (!isset($fields_data[$this->getId()]) || !is_array($fields_data[$this->getId()])) {
            $fields_data[$this->getId()] = array();
        }
        $files_infos = $this->getSubmittedInfoFromFILES();
        if (isset($files_infos['name'][$this->getId()])) {
            $info_keys = array_keys($files_infos); //name, type, error, ...
            $nb = count($files_infos['name'][$this->getId()]);
            for ($i = 0 ; $i < $nb ; ++$i) {
                $tab = array();
                foreach ($info_keys as $key) {
                    $tab[$key] = $files_infos[$key][$this->getId()][$i]['file'];
                }
                if (isset($fields_data[$this->getId()][$i])) {
                    $fields_data[$this->getId()][$i] = array_merge($fields_data[$this->getId()][$i], $tab);
                } else {
                    $fields_data[$this->getId()][] = $tab;
                }
            }
        }
    }

    /**
     * Get the array wich contains files submitted by the user
     *
     * @return array or null if not found
     */
    protected function getSubmittedInfoFromFILES() {
        return isset($_FILES['artifact']) ? $_FILES['artifact'] : null;
    }

    protected $files_info_from_request = null;
    /**
     * Extract the file information (name, error, tmp, ...) from the request
     *
     * @return array Array of file info
     */
    protected function extractFilesFromRequest() {
        if (!$this->files_info_from_request) {
        }
        return $this->files_info_from_request;
    }

    /**
     * Save the value and return the id
     *
     * @param Tracker_Artifact                $artifact                The artifact
     * @param int                             $changeset_value_id      The id of the changeset_value
     * @param mixed                           $value                   The value submitted by the user
     * @param Tracker_Artifact_ChangesetValue $previous_changesetvalue The data previously stored in the db
     *
     * @return boolean
     */
    protected function saveValue($artifact, $changeset_value_id, $value, Tracker_Artifact_ChangesetValue $previous_changesetvalue = null) {
        $save_ok = true;

        $success = array();
        $dao = $this->getValueDao();
        //first save the previous files
        if ($previous_changesetvalue) {
            $previous_fileinfo_ids = array();
            foreach($previous_changesetvalue as $previous_attachment) {
                if (empty($value['delete']) || !in_array($previous_attachment->getId(), $value['delete'])) {
                    $previous_fileinfo_ids[] = $previous_attachment->getId();
                }
            }
            if (count($previous_fileinfo_ids)) {
                $save_ok = $save_ok && $dao->create($changeset_value_id, $previous_fileinfo_ids);
            }
        }

        //Now save the new submitted files
        $current_user = $this->getUserManager()->getCurrentUser();
        $r = new Rule_File();
        foreach ($value as $i => $file_info) {
            if ("$i" != 'delete' && $r->isValid($file_info)) {
                $temporary = $this->getTemporaryFileManager();

                if (isset($file_info['id'])) {
                    $temporary_file = $temporary->getFileByTemporaryName($file_info['id']);
                }

                if (isset($temporary_file)) {
                    $attachment = new Tracker_FileInfo(
                        $temporary_file->getId(),
                        $this,
                        $current_user->getId(),
                        trim($temporary_file->getDescription()),
                        $temporary_file->getName(),
                        $temporary_file->getSize(),
                        $temporary_file->getType()
                    );

                    if ($this->createAttachmentForRest($attachment, $file_info)) {
                        $success[] = $attachment->getId();
                    }

                } else {
                    $submitted_by = $current_user;
                    if (isset($file_info['submitted_by'])) {
                        $submitted_by = $file_info['submitted_by'];
                    }
                    $attachment = new Tracker_FileInfo(
                        null,
                        $this,
                        $submitted_by->getId(),
                        trim($file_info['description']),
                        $file_info['name'],
                        $file_info['size'],
                        $file_info['type']
                    );

                    if ($this->createAttachment($attachment, $file_info)) {
                        $success[] = $attachment->getId();
                    }
                }
            }
        }

        if (count($success)) {
            $save_ok = $save_ok && $dao->create($changeset_value_id, $success);
        }
        return $save_ok;
    }

    protected function createAttachment(Tracker_FileInfo $attachment, $file_info) {
        if ($attachment->save()) {
            $this->initFolder();

            $method   = 'move_uploaded_file';
            $tmp_name = $file_info['tmp_name'];

            if(isset($file_info['id'])) {
                $temporary = new Tracker_SOAP_TemporaryFile($this->getCurrentUser(), $file_info['id']);

                if (!$temporary->exists()) {
                    $attachment->delete();
                    return false;
                }

                $method   = 'rename';
                $tmp_name = $temporary->getPath();

            } else if ($this->isImportOfArtifact($file_info)) {
                $method   = 'copy';
            }

            return $this->moveAttachmentToFinalPlace($attachment, $method, $tmp_name);
        }
        return false;
    }

    private function isImportOfArtifact(array $file_info) {
        return isset($file_info[Tracker_Artifact_XMLImport_XMLImportFieldStrategyAttachment::FILE_INFO_COPY_OPTION]) &&
            $file_info[Tracker_Artifact_XMLImport_XMLImportFieldStrategyAttachment::FILE_INFO_COPY_OPTION];
    }

    private function createAttachmentForRest(Tracker_FileInfo $attachment, $file_info) {
        $this->initFolder();

        $method   = 'move_uploaded_file';
        $tmp_name = $file_info['tmp_name'];

        if(isset($file_info['id'])) {
            $filename  = $file_info['id'];
            $temporary = $this->getTemporaryFileManager();

            $user = $this->getUserManager()->getUserById($attachment->getSubmittedBy());
            if (! $temporary->exists($user, $filename)) {
                $attachment->delete();
                return false;
            }

            $method   = 'rename';
            $tmp_name = $temporary->getPath($user, $filename);

            $temporary->removeTemporaryFileInDBByTemporaryName($filename);

        }

        return $this->moveAttachmentToFinalPlace($attachment, $method, $tmp_name);
    }

    private function initFolder()
    {
        $backend                  = Backend::instance();
        $path                     = $this->getRootPath() . '/thumbnails';
        $no_filter_file_extension = array();

        if (! is_dir($path)) {
            mkdir($path, 0777, true);
            $backend->recurseChownChgrp(
                $this->getGlobalTrackerRootPath(),
                ForgeConfig::get('sys_http_user'),
                ForgeConfig::get('sys_http_user'),
                $no_filter_file_extension
            );
        }
    }

    private function moveAttachmentToFinalPlace(Tracker_FileInfo $attachment, $method, $src_path) {
        if ($method($src_path, $attachment->getPath())) {
            $attachment->postUploadActions();
            return true;
        } else {
            $attachment->delete();
            return false;
        }
    }

    /**
     * @see Tracker_FormElement_Field::hasChanges()
     */
    public function hasChanges(Tracker_Artifact $artifact, Tracker_Artifact_ChangesetValue $old_value, $new_value) {
        //"old" and "new" value are irrelevant in this context.
        //We just have to know if there is at least one file successfully uploaded
        return $this->checkThatAtLeastOneFileIsUploaded($new_value) || !empty($new_value['delete']);
    }

    /**
     * Tells if the field takes two columns
     * Ugly legacy hack to display fields in columns
     *
     * @return boolean
     */
    public function takesTwoColumns() {
        return true;
    }

    /**
     * Get the value of this field
     *
     * @param Tracker_Artifact_Changeset $changeset   The changeset (needed in only few cases like 'lud' field)
     * @param int                        $value_id    The id of the value
     * @param boolean                    $has_changed If the changeset value has changed from the rpevious one
     *
     * @return Tracker_Artifact_ChangesetValue or null if not found
     */
    public function getChangesetValue($changeset, $value_id, $has_changed)
    {
        $file_info_factory = $this->getTrackerFileInfoFactory();

        $files = array();
        $file_value = $this->getValueDao()->searchById($value_id);
        foreach ($file_value as $row) {
            $file = $file_info_factory->getById($row['fileinfo_id']);
            if ($file !== null) {
                $files[] = $file;
            }
        }
        return  new Tracker_Artifact_ChangesetValue_File($value_id, $changeset, $this, $has_changed, $files);
    }

    /**
     * Get the file dao
     *
     * @return Tracker_FileInfoDao
     */
    protected function getFileInfoDao() {
        return new Tracker_FileInfoDao();
    }

    /**
     * Get file info factory
     *
     * @return Tracker_FileInfoFactory
     */
    protected function getFileInfoFactory() {
        return new Tracker_FileInfoFactory(
            $this->getFileInfoDao(),
            Tracker_FormElementFactory::instance(),
            Tracker_ArtifactFactory::instance()
        );
    }

    /**
     * Get available values of this field for SOAP usage
     * Fields like int, float, date, string don't have available values
     *
     * @return mixed The values or null if there are no specific available values
     */
    public function getSoapAvailableValues() {
        return null;
    }

    /**
     * Override default value as it's not possible to import a file via CSV
     *
     * @param type $csv_value
     *
     * @return array
     */
    public function getFieldDataFromCSVValue($csv_value) {
        return array();
    }

    public function getFieldDataFromSoapValue(stdClass $soap_value, Tracker_Artifact $artifact = null) {
        return $this->getFieldData($soap_value->field_value);
    }

    public function getFieldDataFromRESTValue(array $rest_value, Tracker_Artifact $artifact = null) {
        //Transform array to object
        $value = json_decode(json_encode($rest_value), FALSE);

        $this->validateDataFromREST($value);

        $file_manager = $this->getTemporaryFileManager();
        return $file_manager->buildFieldDataForREST($value, $artifact);
    }

    public function getFieldDataFromRESTValueByField($value, Tracker_Artifact $artifact = null) {
        throw new Tracker_FormElement_RESTValueByField_NotImplementedException();
    }

    private function validateDataFromREST($data) {
        if (! property_exists($data, 'value') || ! is_array($data->value)){
            throw new Tracker_FormElement_InvalidFieldException('Invalid format for file field "'.$data->field_id.'". '
                . ' Correct format is {"field_id" : 425, "value" : [457, 258]}');
        }
    }

    /**
     * @return Tracker_Artifact_Attachment_TemporaryFileManager
     */
    private function getTemporaryFileManager() {
        return new Tracker_Artifact_Attachment_TemporaryFileManager(
            $this->getUserManager(),
            new Tracker_Artifact_Attachment_TemporaryFileManagerDao(),
            $this->getTrackerFileInfoFactory(),
            new System_Command(),
            ForgeConfig::get('sys_file_deletion_delay')
        );
    }

    private function getUserManager() {
        return UserManager::instance();
    }

    protected function getTrackerFileInfoFactory()
    {
        return new Tracker_FileInfoFactory(
            new Tracker_FileInfoDao(),
            Tracker_FormElementFactory::instance(),
            Tracker_ArtifactFactory::instance()
        );
    }

    /**
     * Get the field data for artifact submission
     *
     * @param string the soap field value
     *
     * @return String the field data corresponding to the soap_value for artifact submision
     */
    public function getFieldData($soap_value) {
        if (!($soap_value instanceof stdClass)) {
            throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "Invalid submitted value for file field");
        }
        if (!isset($soap_value->file_info) || !is_array($soap_value->file_info)) {
            throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "A File FieldValue must have a 'file_info' array (ArrayOfFieldValueFileInfo)");
        }
        $field_data = array();
        foreach ($soap_value->file_info as $fileinfo) {
            if (!($fileinfo instanceof stdClass)) {
                throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "Fileinfo must be an array of FieldValueFileInfo");
            }
            if (!(isset($fileinfo->description) && isset($fileinfo->filename) && isset($fileinfo->filetype) && isset($fileinfo->filesize))) {
                throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "Fileinfo must be an array of FieldValueFileInfo");
            }
            if (!(isset($fileinfo->id) && $fileinfo->id)) {
                throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "FieldValueFileInfo must have an id of a temporary file");
            }
            if (isset($fileinfo->action) && $fileinfo->action == 'delete') {
                $field_data['delete'][] = $fileinfo->id;
            } else {
                $temporary_file = new Tracker_SOAP_TemporaryFile($this->getCurrentUser(), $fileinfo->id);
                if (! $temporary_file->exists($fileinfo->id)) {
                    throw new SoapFault(self::SOAP_FAULT_INVALID_REQUEST_FORMAT, "Invalid FieldValueFileInfo->id, file doesn't exist");
                }
                $field_data[] = array(
                    'id'          => $fileinfo->id,
                    'description' => $fileinfo->description,
                    'name'        => $fileinfo->filename,
                    'type'        => $fileinfo->filetype,
                    'size'        => $fileinfo->filesize,
                    'error'       => UPLOAD_ERR_OK,
                    'tmp_name'    => $temporary_file->getPath($fileinfo->id),
                );
            }
        }
        return $field_data;
    }

    protected function getTemporaryFileManagerDao() {
        return new Tracker_Artifact_Attachment_TemporaryFileManagerDao();
    }

    public function deleteChangesetValue(Tracker_Artifact_Changeset $changeset, $changeset_value_id) {
        $values = $this->getChangesetValue($changeset, $changeset_value_id, false);
        foreach($values as $fileinfo) {
            $fileinfo->delete();
        }
        parent::deleteChangesetValue($changeset, $changeset_value_id);
    }

    public function accept(Tracker_FormElement_FieldVisitor $visitor) {
        return $visitor->visitFile($this);
    }

    public function isPreviousChangesetEmpty(Tracker_Artifact $artifact, $value) {
        $last_changeset = $artifact->getLastChangeset();

        if ($last_changeset && count($last_changeset->getValue($this)->getFiles()) > 0) {
            return $this->areAllFilesDeletedFromPreviousChangeset($last_changeset, $value);
        }
        return true;
    }

    private function areAllFilesDeletedFromPreviousChangeset($last_changeset, $value) {
        $files = $last_changeset->getValue($this)->getFiles();
        if (isset($value['delete']) && (count($files) == count($value['delete']))) {
            return true;
        }
        return false;
    }

    public function isEmpty($value, $artifact) {
        $is_empty = !$this->checkThatAtLeastOneFileIsUploaded($value);
        if ($is_empty) {
            $is_empty = $this->isPreviousChangesetEmpty($artifact, $value);
        }
        return $is_empty;
    }

    /**
     * @return string
     */
    private function getGlobalTrackerRootPath()
    {
        return ForgeConfig::get('sys_data_dir') . '/tracker/';
    }
}
